接下來,連續三天,我們會每天分享五個Python小技巧。
# 01a
中,x or 'x'
使用的原理為bool(None)
會被視為False
,所以當沒有給定預定值時,self.x
會被設定為'x'
,算是一種常用的語法。但這麼一來,當x
給定任何布林值為False
的obj
時,Python會使用or
之後的'x'
來作為self.x
,包括None
、False
和空的container
等等。
# 01a
class MyClass:
def __init__(self, x=None):
self.x = x or 'x'
if __name__ == '__main__':
my_inst = MyClass(False)
print(my_inst.x) # 'x' (not False)
當不確定參數是否會傳入布林值為False
的obj
時,我們建議使用# 01b
中,顯性判斷其是否為None
的語法。看起來比較不酷沒錯,但是想起那些踩坑時的痛苦,建議還是多打幾個字,畢竟The Zen of Python
都告誡我們Explicit is better than implicit.
。
# 01b
class MyClass:
def __init__(self, x=None):
self.x = x if x is not None else 'x'
if __name__ == '__main__':
my_inst = MyClass(False)
print(my_inst.x) # False
None
是一個不錯的預設值。但當您的程式可以接受None
為一合法輸入值時,又該以什麼值來當預設值呢?事實上,不管你設什麼樣的值,使用者都可能會直接傳入那個值。
一個常見的方法是使用object()
來當預設值,一般我們會稱呼這樣的用法為sentinel
。由於每次的sentinel
是無法預測的,所以除非於寫code時,顯性傳進這個值,才能確定這個值是由自己傳入的。
假設我們想要寫一個get_given
function
,其要求如下:
given
與default
。given
時,該function
需回傳given
,否則即回傳default
,而default
之預設值為0
。# 02a
中以None
為given
的預設值,此時當顯性給予None
時,其仍然會回傳2
,但這不符合我們所希望的行為。
# 02a
def get_given(given=None, default=0):
return default if given is None else given
if __name__ == '__main__':
print(get_given('abc')) # 'abc'
print(get_given(None, 2)) # 2
# 02b
中以object()
為sentinel
來作為given
的預設值,此時只有當顯性傳入sentinel
給given
時,才能打破回傳值不是給予值的情況。
# 02b
sentinel = object()
def get_given(given=sentinel, default=0):
return default if given is sentinel else given
if __name__ == '__main__':
print(get_given('abc')) # 'abc'
print(get_given(None, 2)) # None
# We can only get 2(default) by passing sentinel to given explicitly
print(get_given(sentinel, 2)) # 2
在建立class
的時候,常常會有一種情況是希望於__init__
中選擇性接收一些值,來更新內部的某個mutable
的container
,像是dict
、set
或list
等。
舉例來說,# 03a
中的MyDict
繼承UserDict
(註1
)。於__init__
中接收一個選擇性的dict_data
。當其不為None
時,我們希望能將傳入的值,更新到self
。
# 03a
from collections import UserDict
class MyDict(UserDict):
def __init__(self, dict_data=None):
super().__init__()
if dict_data is not None:
self.update(dict_data)
由於這樣類似的pattern很常出現,我們就開始動動腦,是不是有辦法免除這個if
的確認呢?
首先# 03b
試著將dict_data
設為{}
。修但幾勒,我們了解大家會說你怎麼可以把mutable
的obj
作為預設值呢?難道你沒讀過Python人必看的The Hitchhiker's Guide to Python嗎?但仔細想想,如果沒有設一個instance variable
給dict_data
的話,我們「好像」沒有辦法mutate
它(除非直接mutate
dict_data
)。
# 03b
from collections import UserDict
class MyDict(UserDict):
def __init__(self, dict_data={}):
super().__init__()
self.update(dict_data)
當然,如果您寫成# 03c
的格式,那麼我們就可以藉由self._dict_data
來mutate
dict_data
。變動d._dict_data
也會變動d2._dict_data
,因為兩個是同一個obj
,這樣的行為相信不是您想要的。
# 03c
from collections import UserDict
class MyDict(UserDict):
def __init__(self, dict_data={}):
super().__init__()
self._dict_data = dict_data
self.update(dict_data)
if __name__ == '__main__':
d, d2 = MyDict(), MyDict()
print(d._dict_data is d2._dict_data) # True
print(d, d2) # {}, {}
d._dict_data['a'] = 1
print(d._dict_data is d2._dict_data) # True
print(d._dict_data, d2._dict_data) # {'a': 1} {'a': 1}
# 03d
中,使用空tuple
來當預設值,由於tuple
是immutable
,所以不會有# 03c
的情況,是一個我們覺得可以考慮的寫法,尤其是當這個變數代表iterable
時。
# 03d
from collections import UserDict
class MyDict(UserDict):
def __init__(self, dict_data=()):
super().__init__()
self._dict_data = dict_data
self.update(dict_data)
if __name__ == '__main__':
d, d2 = MyDict(), MyDict()
print(d._dict_data is d2._dict_data) # True
print(d, d2) # {}, {}
d._dict_data = (1,)
print(d._dict_data is d2._dict_data) # False
print(d._dict_data, d2._dict_data) # (1,) ()
當需要formatdatetime
的obj
時,一般會使用datetime.strftime
,如# 04a
。
# 04a
from datetime import datetime
now = datetime.now()
datetime_fmt = '%Y-%m-%d_%H:%M:%S'
if __name__ == '__main__':
now_str = now.strftime(datetime_fmt)
print(f'{now_str}') # 2023-09-01_21:14:41
但其實f-string
是可以認得datetime
format所用的格式,而且使用起來更為方便。# 04b
最後一行的f-string
,我們使用兩層{}
。外層datetime
object
的:
後,可擺入需要format的格式。
# 04b
from datetime import datetime
now = datetime.now()
datetime_fmt = '%Y-%m-%d_%H:%M:%S'
if __name__ == '__main__':
print(f'{now:{datetime_fmt}}') # 2023-09-01_21:14:41
當定義客製的Exception
時,常使用pass
,如# 05a
。
# 05a
class MyError(Exception):
pass
其實有時候也可以考慮使用docstrings
代替pass
,如# 05b
。除了這是個合法的語法外(相當於指定__doc__
),也可以為Exception
增加說明。其它像是定義需要略為說明的class
或是function
時,也可以使用。
# 05b
class BetterMyError(Exception):
"""This error will be raised if..."""
另外,一個有趣的小知識,其實Ellipsis也是合法的語法,如# 05c
。
# 05c
class MyCoolError(Exception):
...
註1:UserDict
相比於繼承dict
更加容易操作,如果有興趣深入研究的朋友,可以參考Trey的說明或是Python docs。